/*
 * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/message/BasicLineParser.java $
 * $Revision: 591798 $
 * $Date: 2007-11-04 08:19:29 -0800 (Sun, 04 Nov 2007) $
 *
 * ====================================================================
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package org.apache.http.message;

import org.apache.http.Header;
import org.apache.http.HttpVersion;
import org.apache.http.ParseException;
import org.apache.http.ProtocolVersion;
import org.apache.http.RequestLine;
import org.apache.http.StatusLine;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.CharArrayBuffer;

/**
 * Basic parser for lines in the head section of an HTTP message. There are
 * individual methods for parsing a request line, a status line, or a header
 * line. The lines to parse are passed in memory, the parser does not depend on
 * any specific IO mechanism. Instances of this class are stateless and
 * thread-safe. Derived classes MUST maintain these properties.
 * 
 * <p>
 * Note: This class was created by refactoring parsing code located in various
 * other classes. The author tags from those other classes have been replicated
 * here, although the association with the parsing code taken from there has not
 * been traced.
 * </p>
 * 
 * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a>
 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
 * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a>
 * @author and others
 */
public class BasicLineParser implements LineParser {

    /**
     * A default instance of this class, for use as default or fallback. Note
     * that {@link BasicLineParser} is not a singleton, there can be many
     * instances of the class itself and of derived classes. The instance here
     * provides non-customized, default behavior.
     */
    public final static BasicLineParser DEFAULT = new BasicLineParser();

    /**
     * A version of the protocol to parse. The version is typically not
     * relevant, but the protocol name.
     */
    protected final ProtocolVersion protocol;

    /**
     * Creates a new line parser for the given HTTP-like protocol.
     * 
     * @param proto
     *            a version of the protocol to parse, or <code>null</code> for
     *            HTTP. The actual version is not relevant, only the protocol
     *            name.
     */
    public BasicLineParser(ProtocolVersion proto) {
        if (proto == null) {
            proto = HttpVersion.HTTP_1_1;
        }
        this.protocol = proto;
    }

    /**
     * Creates a new line parser for HTTP.
     */
    public BasicLineParser() {
        this(null);
    }

    public final static ProtocolVersion parseProtocolVersion(String value,
            LineParser parser) throws ParseException {

        if (value == null) {
            throw new IllegalArgumentException(
                    "Value to parse may not be null.");
        }

        if (parser == null)
            parser = BasicLineParser.DEFAULT;

        CharArrayBuffer buffer = new CharArrayBuffer(value.length());
        buffer.append(value);
        ParserCursor cursor = new ParserCursor(0, value.length());
        return parser.parseProtocolVersion(buffer, cursor);
    }

    // non-javadoc, see interface LineParser
    public ProtocolVersion parseProtocolVersion(final CharArrayBuffer buffer,
            final ParserCursor cursor) throws ParseException {

        if (buffer == null) {
            throw new IllegalArgumentException(
                    "Char array buffer may not be null");
        }
        if (cursor == null) {
            throw new IllegalArgumentException("Parser cursor may not be null");
        }

        final String protoname = this.protocol.getProtocol();
        final int protolength = protoname.length();

        int indexFrom = cursor.getPos();
        int indexTo = cursor.getUpperBound();

        skipWhitespace(buffer, cursor);

        int i = cursor.getPos();

        // long enough for "HTTP/1.1"?
        if (i + protolength + 4 > indexTo) {
            throw new ParseException("Not a valid protocol version: "
                    + buffer.substring(indexFrom, indexTo));
        }

        // check the protocol name and slash
        boolean ok = true;
        for (int j = 0; ok && (j < protolength); j++) {
            ok = (buffer.charAt(i + j) == protoname.charAt(j));
        }
        if (ok) {
            ok = (buffer.charAt(i + protolength) == '/');
        }
        if (!ok) {
            throw new ParseException("Not a valid protocol version: "
                    + buffer.substring(indexFrom, indexTo));
        }

        i += protolength + 1;

        int period = buffer.indexOf('.', i, indexTo);
        if (period == -1) {
            throw new ParseException("Invalid protocol version number: "
                    + buffer.substring(indexFrom, indexTo));
        }
        int major;
        try {
            major = Integer.parseInt(buffer.substringTrimmed(i, period));
        } catch (NumberFormatException e) {
            throw new ParseException("Invalid protocol major version number: "
                    + buffer.substring(indexFrom, indexTo));
        }
        i = period + 1;

        int blank = buffer.indexOf(' ', i, indexTo);
        if (blank == -1) {
            blank = indexTo;
        }
        int minor;
        try {
            minor = Integer.parseInt(buffer.substringTrimmed(i, blank));
        } catch (NumberFormatException e) {
            throw new ParseException("Invalid protocol minor version number: "
                    + buffer.substring(indexFrom, indexTo));
        }

        cursor.updatePos(blank);

        return createProtocolVersion(major, minor);

    } // parseProtocolVersion

    /**
     * Creates a protocol version. Called from {@link #parseProtocolVersion}.
     * 
     * @param major
     *            the major version number, for example 1 in HTTP/1.0
     * @param minor
     *            the minor version number, for example 0 in HTTP/1.0
     * 
     * @return the protocol version
     */
    protected ProtocolVersion createProtocolVersion(int major, int minor) {
        return protocol.forVersion(major, minor);
    }

    // non-javadoc, see interface LineParser
    public boolean hasProtocolVersion(final CharArrayBuffer buffer,
            final ParserCursor cursor) {

        if (buffer == null) {
            throw new IllegalArgumentException(
                    "Char array buffer may not be null");
        }
        if (cursor == null) {
            throw new IllegalArgumentException("Parser cursor may not be null");
        }
        int index = cursor.getPos();

        final String protoname = this.protocol.getProtocol();
        final int protolength = protoname.length();

        if (buffer.length() < protolength + 4)
            return false; // not long enough for "HTTP/1.1"

        if (index < 0) {
            // end of line, no tolerance for trailing whitespace
            // this works only for single-digit major and minor version
            index = buffer.length() - 4 - protolength;
        } else if (index == 0) {
            // beginning of line, tolerate leading whitespace
            while ((index < buffer.length())
                    && HTTP.isWhitespace(buffer.charAt(index))) {
                index++;
            }
        } // else within line, don't tolerate whitespace

        if (index + protolength + 4 > buffer.length())
            return false;

        // just check protocol name and slash, no need to analyse the version
        boolean ok = true;
        for (int j = 0; ok && (j < protolength); j++) {
            ok = (buffer.charAt(index + j) == protoname.charAt(j));
        }
        if (ok) {
            ok = (buffer.charAt(index + protolength) == '/');
        }

        return ok;
    }

    public final static RequestLine parseRequestLine(final String value,
            LineParser parser) throws ParseException {

        if (value == null) {
            throw new IllegalArgumentException(
                    "Value to parse may not be null.");
        }

        if (parser == null)
            parser = BasicLineParser.DEFAULT;

        CharArrayBuffer buffer = new CharArrayBuffer(value.length());
        buffer.append(value);
        ParserCursor cursor = new ParserCursor(0, value.length());
        return parser.parseRequestLine(buffer, cursor);
    }

    /**
     * Parses a request line.
     * 
     * @param buffer
     *            a buffer holding the line to parse
     * 
     * @return the parsed request line
     * 
     * @throws ParseException
     *             in case of a parse error
     */
    public RequestLine parseRequestLine(final CharArrayBuffer buffer,
            final ParserCursor cursor) throws ParseException {

        if (buffer == null) {
            throw new IllegalArgumentException(
                    "Char array buffer may not be null");
        }
        if (cursor == null) {
            throw new IllegalArgumentException("Parser cursor may not be null");
        }

        int indexFrom = cursor.getPos();
        int indexTo = cursor.getUpperBound();

        try {
            skipWhitespace(buffer, cursor);
            int i = cursor.getPos();

            int blank = buffer.indexOf(' ', i, indexTo);
            if (blank < 0) {
                throw new ParseException("Invalid request line: "
                        + buffer.substring(indexFrom, indexTo));
            }
            String method = buffer.substringTrimmed(i, blank);
            cursor.updatePos(blank);

            skipWhitespace(buffer, cursor);
            i = cursor.getPos();

            blank = buffer.indexOf(' ', i, indexTo);
            if (blank < 0) {
                throw new ParseException("Invalid request line: "
                        + buffer.substring(indexFrom, indexTo));
            }
            String uri = buffer.substringTrimmed(i, blank);
            cursor.updatePos(blank);

            ProtocolVersion ver = parseProtocolVersion(buffer, cursor);

            skipWhitespace(buffer, cursor);
            if (!cursor.atEnd()) {
                throw new ParseException("Invalid request line: "
                        + buffer.substring(indexFrom, indexTo));
            }

            return createRequestLine(method, uri, ver);
        } catch (IndexOutOfBoundsException e) {
            throw new ParseException("Invalid request line: "
                    + buffer.substring(indexFrom, indexTo));
        }
    } // parseRequestLine

    /**
     * Instantiates a new request line. Called from {@link #parseRequestLine}.
     * 
     * @param method
     *            the request method
     * @param uri
     *            the requested URI
     * @param ver
     *            the protocol version
     * 
     * @return a new status line with the given data
     */
    protected RequestLine createRequestLine(final String method,
            final String uri, final ProtocolVersion ver) {
        return new BasicRequestLine(method, uri, ver);
    }

    public final static StatusLine parseStatusLine(final String value,
            LineParser parser) throws ParseException {

        if (value == null) {
            throw new IllegalArgumentException(
                    "Value to parse may not be null.");
        }

        if (parser == null)
            parser = BasicLineParser.DEFAULT;

        CharArrayBuffer buffer = new CharArrayBuffer(value.length());
        buffer.append(value);
        ParserCursor cursor = new ParserCursor(0, value.length());
        return parser.parseStatusLine(buffer, cursor);
    }

    // non-javadoc, see interface LineParser
    public StatusLine parseStatusLine(final CharArrayBuffer buffer,
            final ParserCursor cursor) throws ParseException {

        if (buffer == null) {
            throw new IllegalArgumentException(
                    "Char array buffer may not be null");
        }
        if (cursor == null) {
            throw new IllegalArgumentException("Parser cursor may not be null");
        }

        int indexFrom = cursor.getPos();
        int indexTo = cursor.getUpperBound();

        try {
            // handle the HTTP-Version
            ProtocolVersion ver = parseProtocolVersion(buffer, cursor);

            // handle the Status-Code
            skipWhitespace(buffer, cursor);
            int i = cursor.getPos();

            int blank = buffer.indexOf(' ', i, indexTo);
            if (blank < 0) {
                blank = indexTo;
            }
            int statusCode = 0;
            try {
                statusCode = Integer
                        .parseInt(buffer.substringTrimmed(i, blank));
            } catch (NumberFormatException e) {
                throw new ParseException(
                        "Unable to parse status code from status line: "
                                + buffer.substring(indexFrom, indexTo));
            }
            // handle the Reason-Phrase
            i = blank;
            String reasonPhrase = null;
            if (i < indexTo) {
                reasonPhrase = buffer.substringTrimmed(i, indexTo);
            } else {
                reasonPhrase = "";
            }
            return createStatusLine(ver, statusCode, reasonPhrase);

        } catch (IndexOutOfBoundsException e) {
            throw new ParseException("Invalid status line: "
                    + buffer.substring(indexFrom, indexTo));
        }
    } // parseStatusLine

    /**
     * Instantiates a new status line. Called from {@link #parseStatusLine}.
     * 
     * @param ver
     *            the protocol version
     * @param status
     *            the status code
     * @param reason
     *            the reason phrase
     * 
     * @return a new status line with the given data
     */
    protected StatusLine createStatusLine(final ProtocolVersion ver,
            final int status, final String reason) {
        return new BasicStatusLine(ver, status, reason);
    }

    public final static Header parseHeader(final String value, LineParser parser)
            throws ParseException {

        if (value == null) {
            throw new IllegalArgumentException("Value to parse may not be null");
        }

        if (parser == null)
            parser = BasicLineParser.DEFAULT;

        CharArrayBuffer buffer = new CharArrayBuffer(value.length());
        buffer.append(value);
        return parser.parseHeader(buffer);
    }

    // non-javadoc, see interface LineParser
    public Header parseHeader(CharArrayBuffer buffer) throws ParseException {

        // the actual parser code is in the constructor of BufferedHeader
        return new BufferedHeader(buffer);
    }

    /**
     * Helper to skip whitespace.
     */
    protected void skipWhitespace(final CharArrayBuffer buffer,
            final ParserCursor cursor) {
        int pos = cursor.getPos();
        int indexTo = cursor.getUpperBound();
        while ((pos < indexTo) && HTTP.isWhitespace(buffer.charAt(pos))) {
            pos++;
        }
        cursor.updatePos(pos);
    }

} // class BasicLineParser
